import * as React from 'react';
const {
  PureComponent,
  PropTypes,
} = React;
import {findDOMNode} from 'react-dom';
import './uploader.scss';
import * as assign from 'object-assign';
import {post} from '../../util/ajax';
import 'blueimp-canvastoblob';
import {Mixin as FormsyMixin} from 'formsy-react';
import {decorate as Mixin} from 'react-mixin';
import * as isPlainObject from 'lodash/isPlainObject';
import * as qs from 'qs';
import {alert} from '../alert/alert';
import {formatFileSize} from './image';
import * as Promise from 'promise';
import * as mapLimit from 'async/mapLimit';
import * as cx from 'classnames';
import * as find from 'lodash/find';

const uploadFile = (file, reciever, params, config = {}) => {
  const fd = new FormData();

  if (/^\/api\/(?:page\/)?proxy/.test(reciever)) {
    fd.append('file', file);
    fd.append('fieldName', config.fieldName);
  } else {
    fd.append(config.fieldName || 'file', file);
  }

  const idx = reciever.indexOf('?');

  if (~idx && params) {
    params = assign(qs.parse(reciever, idx + 1), params);
    reciever = reciever.substring(0, idx) + '?' + qs.stringify(params);
  } else if (params) {
    reciever += '?' + qs.stringify(params);
  }

  return post(reciever, fd, {
    withCredentials: true
  });
};

const uploadBigFile = (file, reciever, params, config = {}) => {
  const chunkSize = config.chunkSize || 5 * 1024 * 1024;
  return new Promise((resolve, reject) => {
    let state;

    post(config.startChunkApi, {filename: file.name})
      .then(startChunk)
      .catch(reject);

    function startChunk(ret) {
      const tasks = getTasks(file);

      state = {
        key: ret.data.key,
        uploadId: ret.data.uploadId,
        loaded: 0,
        total: tasks.length
      };

      mapLimit(tasks, 3, uploadPartFile(state, config), function (err, results) {
        if (err) {
          reject(err);
        } else {
          finishChunk(results, state);
        }
      });
    }

    function finishChunk(partList, state) {
      post(config.finishChunkApi, assign({}, params, {
        uploadId: state.uploadId,
        key: state.key,
        filename: file.name,
        partList
      })).then(resolve).catch(reject);
    }

    function uploadPartFile(state, conf) {
      reciever = conf.chunkApi;

      return (task, callback) => {
        const fd = new FormData();
        let blob = task.file.slice(task.start, task.stop + 1);

        fd.append('key', state.key);
        fd.append('uploadId', state.uploadId);
        fd.append('partNumber', task.partNumber);
        fd.append('partSize', task.partSize);


        if (/^\/api\/(?:page\/)?proxy/.test(reciever)) {
          fd.append('file', blob, file.name);
          fd.append('fieldName', config.fieldName);
        } else {
          fd.append(config.fieldName || 'file', blob, file.name);
        }

        return post(reciever, fd, {
            withCredentials: true
          })
          .then(ret => {
            state.loaded++;
            callback(null, {
              partNumber: task.partNumber,
              eTag: ret.data.eTag
            });
          })
          .catch(callback);
      }
    }

    function getTasks(file) {
      let leftSize = file.size;
      let offset = 0;
      let partNumber = 1;

      let tasks = [];

      while (leftSize > 0) {
        let partSize = Math.min(leftSize, chunkSize);
        tasks.push({
          file: file,
          partNumber: partNumber,
          partSize: partSize,
          start: offset,
          stop: offset + partSize - 1
        });

        leftSize -= partSize;
        offset += partSize;
        partNumber += 1;
      }

      return tasks;
    }
  })
};

const valueToFile = (value, props) => {
  return value ? assign({
    state: 'init'
  }, typeof value == 'string' ? {
      value,
      url: typeof props.downloadUrl === 'string' && value ? `${props.downloadUrl}${value}` : null
    } : value) : null;
};

const notNull = item => item;

export default class Uploader extends PureComponent {
  static propTypes = {
    reciever: PropTypes.string.isRequired,
    downloadUrl: PropTypes.string,
    fileField: PropTypes.string
  };

  static defaultProps = {
    accept: 'text/plain',
    fileField: 'file',
    btnClassName: 'btn-info',
    btnUploadClassName: 'btn-success',
    maxSize: 0,
    stateTextMap: {
      'init': "",
      'pending': "等待上传",
      'uploading': "上传中",
      'error': "上传出错",
      'uploaded': "已上传"
    },
    autoUpload: true,
    multiple: false,
    maxLength: 0,
    hideUploadButton: false
  };

  current = null;

  constructor(props) {
    super(props);

    const value = props.value;
    const multiple = props.multiple;


    this.state = {
      files: (multiple ? Array.isArray(value) ? value : [] : [value]).map(item => valueToFile(item, props)).filter(notNull),
      uploading: false
    };

    this.updateFile = this.updateFile.bind(this);
    this.removeFile = this.removeFile.bind(this);
    this.clearError = this.clearError.bind(this);
    this.handleDrop = this.handleDrop.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.startUpload = this.startUpload.bind(this);
    this.stopUpload = this.stopUpload.bind(this);
    this.toggleUpload = this.toggleUpload.bind(this);
    this.tick = this.tick.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    const props = this.props;

    if (props.value !== nextProps.value) {
      const value = nextProps.value;
      const multiple = nextProps.multiple;

      this.setState({
        files: (multiple ? value : [value]).map(item => {
          let obj = valueToFile(item, nextProps);
          let org;

          if (obj && (org = find(this.state.files, item => item.value === obj.value))) {
            obj = assign({}, org, obj);
          }

          return obj;
        }).filter(notNull),
      });
    }
  }

  handleDrop(e) {
    const files = e.currentTarget.files;

    if (!files.length) {
      return;
    }

    const {maxSize, multiple, maxLength} = this.props;
    const allowed = (multiple ? maxLength ? maxLength : (files.length + this.state.files.length) : 1) - this.state.files.length;

    const inputFiles = [];

    [].slice.call(files, 0, allowed).forEach(file => {
      if (maxSize && file.size > maxSize) {
        alert(`您选择的文件 ${file.name} 大小为 ${formatFileSize(file.size)} 超出了最大为 ${formatFileSize(maxSize)} 的限制，请重新选择`);
        return null;
      }

      file.state = 'pending';
      inputFiles.push(file);
    });

    if (!inputFiles.length) {
      return;
    }

    this.setState({
      error: null,
      files: this.state.files.concat(inputFiles)
    }, () => {
      const {
        autoUpload,
        onRequestLock
      } = this.props;

      if (autoUpload) {
        this.startUpload();
      } else if (onRequestLock) {
        onRequestLock(this.startUpload);
      }
    });
  }

  startUpload() {
    if (this.state.uploading) {
      return;
    }

    this.setState({
      uploading: true,
      files: this.state.files.map(file => {
        if (file.state === 'error') {
          file.state = 'pending';
        }

        return file;
      })
    }, () => {
      const {onRequestLock} = this.props;

      onRequestLock && onRequestLock();
      this.tick();
    });
  }

  toggleUpload() {
    return this.state.uploading ? this.stopUpload() : this.startUpload();
  }

  stopUpload() {
    if (!this.state.uploading) {
      return;
    }

    this.setState({
      uploading: false
    });
  }

  tick() {
    if (this.current || !this.state.uploading) {return;}

    const file = find(this.state.files, item => item.state === 'pending');
    if (file) {
      this.current = file;

      file.state = 'uploading';
      this.setState({
        files: this.state.files.concat()
      }, () => this.updateFile(file, (error, file, obj) => {
        const files = this.state.files.concat();
        const idx = files.indexOf(file);

        if (!~idx) {
          return;
        }

        let newFile = file;

        if (error) {
          newFile.state = 'error';
          newFile.error = error;
        } else {
          newFile = obj;
        }
        files.splice(idx, 1, newFile);
        this.current = null;
        this.setState({
          files: files
        }, this.tick);
      }));
    } else {
      this.setState({
        uploading: false
      }, () => {
        const {
          onRequestUnLock
        } = this.props;

        onRequestUnLock && onRequestUnLock();

        this.onChange();
      });
    }
  }


  updateFile(file, cb, progress) {
    const {
      reciever,
      fileField,
      downloadUrl,
      useChunk,
      chunkSize,
      startChunkApi,
      chunkApi,
      finishChunkApi
    } = this.props;

    let fn = useChunk === 'auto' && file.size > chunkSize || useChunk === true ? uploadBigFile : uploadFile;

    fn(file, reciever, null, {
      fieldName: fileField,
      chunkSize,
      startChunkApi,
      chunkApi,
      finishChunkApi,
      progress
    })
    .then(ret => {
      if (ret.status) {
        throw new Error(ret.msg || '上传失败, 请重试');
      }

      const value = ret.data.value || ret.data;

      cb(null, file, assign({}, isPlainObject(ret.data) ? ret.data : null, {
        value: value,
        url: typeof downloadUrl === 'string' && value ? `${downloadUrl}${value}` : ret.data ? ret.data.url : null,
        state: 'uploaded'
      }));
    })
    .catch(error => {
      cb(error.message || '上传失败, 请重试', file);
    })
  }

  handleClick() {
    this.refs.dropzone.open();
  }

  removeFile(file, index) {
    const files = this.state.files.concat();

    files.splice(index, 1);

    this.setState({
      files: files
    }, () => {
      if (file.state === 'uploaded' || file.state === 'init') {
        this.onChange();
      }
    });
  }

  clearError() {
    this.setState({
      error: null
    });
  }

  onChange() {
    const {
      multiple,
      onChange
    } = this.props;

    const files = this.state.files.filter(file => file.state == 'uploaded' || file.state == 'init');

    onChange(multiple ? files : files[0]);
  }

  render() {
    const {
      btnLabel,
      accept,
      disabled,
      btnClassName,
      btnUploadClassName,
      maxLength,
      multiple,
      autoUpload,
      stateTextMap,
      hideUploadButton
    } = this.props;
    let {
      files,
      uploading,
      error
    } = this.state;

    const hasPending = files.some(file => file.state == 'pending');

    return (
      <div>
        {error ? (
          <div>
            <p className="help-block text-danger inline">{error}</p>
            <a className="btn btn-link" onClick={this.clearError}><i className="fa fa-times" /></a>
          </div>
        ) : null}

        {files && files.length ? (
          <ul className="list-group no-bg m-b-sm">
            {files.map((file, key) => (
              <li key={key} className="list-group-item clearfix">
                <a
                  className="text-danger pull-right"
                  onClick={disabled ? null : () => this.removeFile(file, key)}
                ><i className="fa fa-times" /></a>
                <span className="pull-right text-muted text-xs m-r-sm">{stateTextMap[file.state] || ''}</span>
                <i className="fa fa-file fa-fw m-r-xs" />
                {file.url ? (<a href={file.url} target="_blank">{file.name || file.filename || file.value}</a>) : (<span>{file.name || file.filename}</span>)}
              </li>
            ))}
          </ul>
        ) : null}

        <div className="clear">
          {multiple && (!maxLength || files.length < maxLength) || !multiple && !files.length ? (
              <label className={cx("btn m-r-xs", btnClassName, {disabled})}>
                <input type="file" accept={accept} multiple={multiple} className="invisible" onChange={this.handleDrop} />
                {btnLabel}
              </label>
            ) : null}

          {!autoUpload && !hideUploadButton && files.length ? (
              <button className={cx('btn m-r-xs', btnUploadClassName)} disabled={!hasPending} onClick={this.toggleUpload}>
                {uploading ? '暂停上传' : '开始上传'}
              </button>
            ) : null}

          {this.state.uploading ? (<i className="fa fa-spinner fa-spin fa-2x fa-fw" />) : null}
        </div>
      </div>
    );
  }
}

@Mixin(FormsyMixin)
export class FormsyInput extends PureComponent {

  constructor(props) {
    super(props);

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(value) {
    this.setValue(value && value.url || value);
  }

  render() {
    return <Uploader {...this.props} onChange={this.handleChange} />
  }
}
